[Previous] [Next]

Refining the ActiveX Control

Adding a UserControl object to the current project and placing some constituent controls on it is just the first step toward the creation of a full-fledged, commercial-quality ActiveX control. In this section, I'll show you how to implement a robust user interface, add binding capabilities and property pages, create user-drawn controls, and prepare your controls for the Internet.

Custom Properties

You've already seen how you can add custom properties using pairs of property procedures. This section explains how to implement some special types of properties.

Design-time and run-time properties

Not all properties are available both at design time and at run time, and it's interesting to see how you write the code in the UserControl module to limit the visibility of properties. The easiest way to create a run time-only property, such as the SelText property of a TextBox or the ListIndex property of a ListBox, is by ticking the Don't Show In Property Browser option in the Attributes section of the Procedure Attributes dialog box. (You can access this dialog box by choosing it from the Tools menu.) If this check box is selected, the property doesn't appear in the Properties window at design time.

The problem with this simple approach, however, is that it also hides the property in the other property browser that Visual Basic provides, namely the Locals window. To have the property listed in the Locals window at run time but not in the Properties window, you must raise an error in the Property Get procedure at design time, as this code demonstrates:

Public Property Get SelText() As String
    If Ambient.UserMode = False Then Err.Raise 387
    SelText = Text1.SelText
End Property

Error 387 "Set not permitted" is the error that by convention you should raise in this case, but any error will do the trick. If Visual Basic—or more generally, the host environment—receives an error when reading a value at design time, the property isn't displayed in the properties browser, which is precisely what you want. Creating a property that's unavailable at design time and read-only at run time is even simpler because you need merely to omit the Property Let procedure, as you would do with any read-only property. Visual Basic doesn't show such a property in the Properties window because it couldn't be modified in any way.

Another common situation concerns properties that are available at design time and read-only at run time. This is similar to the MultiLine and ScrollBars properties of the Visual Basic TextBox control. You can implement such properties by raising Error 382 "Set not supported at runtime" in their Property Let procedures, as shown in the following code:

' This property is available at design time and read-only at run time.
Public Property Get ScrollBars() As Integer
    ScrollBars = m_ScrollBars 
End Property
Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
    If Ambient.UserMode Then Err.Raise 382
    m_ScrollBars = New_ScrollBars
    PropertyChanged "ScrollBars"
End Property

When you have design-time properties that are read-only at run time, you can't call the Property Let procedure from within the ReadProperties event procedure because you would get an error. In this case, you're forced to directly assign the private member variable or the constituent control's property, or you have to provide a module-level Boolean variable that you set to True on entering the ReadProperties event and reset to False on exit. You then query this variable before raising errors in the Property Let procedure. You can also use the same variable to skip an unnecessary call to the PropertyChanged method, as in this code example:

Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
    ' The ReadingProperties variable is True if this routine is being
    ' called from within the ReadProperties event procedure.
    If Ambient.UserMode 
And Not ReadingProperties Then Err.Raise 382
    m_ScrollBars = New_ScrollBars
    If Not ReadingProperties Then 
PropertyChanged "ScrollBars"
End Property

Enumerated properties

You can define enumerated properties using either Enum blocks in code or Visual Basic's own enumerated types. For example, you can modify the code produced by the wizard and improve the MousePointer property as follows:

Public Property Get MousePointer() As MousePointerConstants
    MousePointer = Text1.MousePointer
End Property
Public Property Let MousePointer(ByVal New_MousePointer _
    As MousePointerConstants)
    Text1.MousePointer() = New_MousePointer
    PropertyChanged "MousePointer"
End Property

Enumerated properties are useful because their valid values appear in the Properties window in a combo box, as shown in Figure 17-6. Keep in mind, however, that you should always protect your ActiveX control from invalid assignments in code, so the previous routine should be rewritten as follows:

Public Property Let MousePointer(ByVal New_MousePointer _
    As MousePointerConstants)
    Select Case New_MousePointer
        Case vbDefault To vbSizeAll, vbCustom
            Text1.MousePointer() = New_MousePointer
            PropertyChanged "MousePointer"
        Case Else
            Err.Raise 380   ' Invalid Property Value error
    End Select
End Property

Figure 17-6. Use enumerated properties to offer a list of valid values in the Properties window.

There's a good reason for not defining properties and arguments using Visual Basic and VBA enumerated constants, though: If you use the control with environments other than Visual Basic, these symbolic constants won't be visible to the client application.

TIP
Sometimes you might want to add spaces and other symbols inside an enumerated value to make it more readable in the Properties window. For example, the FillStyle property includes values such as Horizontal Line or Diagonal Cross. To expose similar values in your ActiveX controls, you have to enclose Enum constants within square brackets, as in the following code:

Enum MyColors
    Black = 1
    [Dark Gray]
    [Light Gray]
    White 
End Enum

TIP
Here's another idea that you might find useful: If you use an enumerated constant name whose name begins with an underscore, such as [_HiddenValue], this value won't appear by default in the Object Browser. However, this value does appear in the Properties window, so this trick is especially useful for enumerated properties that aren't available at design time.

Picture and Font properties

Visual Basic deals in a special way with properties that return a Picture or Font object. In the former instance, the Properties window shows a button that lets you select an image from disk; in the latter, the Properties window includes a button that displays a Font common dialog box.

When working with Font properties, you should keep in mind that they return object references. For example, if two or more constituent controls have been assigned the same Font reference, changing a font attribute in one of them also changes the appearance of all the others. For this reason, Ambient.Font returns a copy of the parent form's font so that any subsequent change to the form's font doesn't affect the UserControl's constituent controls, and vice versa. (If you want to keep your control's font in sync with the form's font, you simply need to trap the AmbientChanged event.) Sharing object references can cause some subtle errors in your code. Consider the following example:

' Case 1: Label1 and Text1 use fonts with identical attributes.
Set Label1.Font = Ambient.Font
Set Text1.Font = Ambient.Font

' Case 2: Label1 and Text1 point to the *same* font.
Set Label1.Font = Ambient.Font
Set Text1.Font = Label1.Font

The two pieces of code look similar, but in the first instance the two constituent controls are assigned different copies of the same font, so you can change the font attributes of one control without affecting the other. In the latter case, both controls are pointing to the same font, so each time you modify a font attribute in either control the other one is affected as well.

It's a common practice to provide all the alternate, old-styled Fontxxxx properties, namely FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru. But you should also make these properties unavailable at design time, and you shouldn't save them in the WriteProperties event if you also save the Font object. If you decide to save individual Fontxxxx properties, it's important that you retrieve them in the correct order (first FontName, and then all the others).

One more thing to keep in mind when dealing with font properties: You can't restrict the choices of the programmer who's using the control to a family of fonts— for example, to nonproportional fonts or to printer fonts—if the Font property is exposed in the Properties window. The only way to restrict font selection is to show a Font Common Dialog box from a Property Page. See the "Property Pages" section later in this chapter for details about building property pages.

Font properties pose a special challenge to ActiveX control programmers. If your control exposes a Font property and the client code modifies one or more font attributes, Visual Basic calls the Property Get Font procedure but not the Property Set Font procedure. If the Font property delegates to a single constituent control, this isn't usually a problem because the control's appearance is correctly updated. Things are different in user-drawn ActiveX controls because in this case your control gets no notification that it should be repainted. This problem has been solved in Visual Basic 6 with the FontChanged event of the StdFont object. Here's a fragment of code taken from a Label-like, user-drawn control that correctly refreshes itself when the client modifies an attribute of the Font property:

Private WithEvents UCFont As StdFont

Private Sub UserControl_InitProperties()
    ' Initialize the Font property (and the UCFont object).
    Set Font = Ambient.Font
End Sub

Public Property Get Font() As Font
    Set Font = UserControl.Font
End Property
Public Property Set Font(ByVal New_Font As Font)
    Set UserControl.Font = New_Font
    Set UCFont = New_Font         ' Prepare to trap events.
    PropertyChanged "Font"
    Refresh                       ' Manually perform the first refresh.
End Property

' This event fires when the client code changes a font's attribute.
Private Sub UCFont_FontChanged(ByVal PropertyName As String)
    Refresh                       ' This causes a Paint event.
End Sub
' Repaint the control.
Private Sub UserControl_Paint()
    Cls
    Print Caption;
End Sub

Object properties

You can create ActiveX controls with properties that return objects, such as a TreeView-like control that exposes a Nodes collection. This is possible because ActiveX control projects can include PublicNotCreatable classes, so your control can internally create them using the New operator and return a reference to its clients through a read-only property. Object properties can be treated as if they were regular properties in most circumstances, but they require particular attention when you need to make them persistent and reload them in the WriteProperties and ReadProperties procedures.

Even if Visual Basic 6 does support persistable classes, you can't save objects that aren't creatable, as in this case. But nothing prevents you from manually creating a PropertyBag object and loading it with all the properties of the dependent object. Let me demonstrate this technique with an example.

Suppose that you have an AddressOCX ActiveX control that lets the user enter a person's name and address, as shown in Figure 17-7. Instead of many properties, this AddressOCX control exposes one object property, named Address, whose class is defined inside the same project. Rather than having the main UserControl module save and reload the individual properties of the dependent object, you should create a Friend property in the PublicNotCreatable class. I usually call this property AllProperties because it sets and returns the values of all the properties in one Byte array. To serialize the properties into an array, I use a private stand-alone PropertyBag object. Following is the complete source code of the Address class module. (For the sake of simplicity, properties are implemented as Public variables.)

' The Address.cls class module
Public Name As String, Street As String
Public City As String, Zip As String, State As String

Friend Property Get AllProperties() As Byte()
    Dim PropBag As New PropertyBag
    PropBag.WriteProperty "Name", Name, ""
    PropBag.WriteProperty "Street", Street, ""
    PropBag.WriteProperty "City", City, ""
    PropBag.WriteProperty "Zip", Zip, ""
    PropBag.WriteProperty "State", State, ""
    AllProperties = PropBag.Contents
End Property
Friend Property Let AllProperties(value() As Byte)
    Dim PropBag As New PropertyBag
    PropBag.Contents = value()
    Name = PropBag.ReadProperty("Name", "")
    Street = PropBag.ReadProperty("Street", "")
    City = PropBag.ReadProperty("City", "")
    Zip = PropBag.ReadProperty("Zip", "")
    State = PropBag.ReadProperty("State", "")
End Property

Rather than saving and reloading all the individual properties in the WriteProperties and ReadProperties event procedures of the main AddressOCX module, you simply save and restore the AllProperties property of the Address object.

Click to view at full size.

Figure 17-7. An AddressOCX ActiveX control that exposes each of the Address properties as an individual Address, PublicNotCreatableobject.

' The AddressOCX code module (partial listing)
Dim m_Address As New Address

Public Property Get Address() As Address
    Set Address = m_Address
End Property
Public Property Set Address(ByVal New_Address As Address)
    Set m_Address = New_Address
    PropertyChanged "Address"
End Property

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    m_Address.AllProperties = PropBag.ReadProperty("Address")
End Sub

Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    Call PropBag.WriteProperty("Address", m_Address.AllProperties)
End Sub

All the individual constituent controls must refer to the corresponding property in the Address object. For example, this is the code in the Change event procedure of the txtName control:

Private Sub txtName_Change()
    Address.Name = txtName
    PropertyChanged "Address"
End Sub

The ActiveX control should also expose a Refresh method that reloads all the values from the Address object into the individual fields. Alternatively, you might implement an event that the Address object raises in the AddressOCX module when any of its properties is assigned a new value. This problem is similar to the one I described in the "Forms as Object Viewers" section of Chapter 9.

Properties that return UDTs

ActiveX controls can expose properties and methods that return user-defined types or that accept UDTs as arguments. Because ActiveX controls are in-process COM components, you can always marshal UDTs regardless of the operating system version. For more details, see the "Passing Data Between Applications" section of Chapter 16.

This feature hasn't been completely ironed out, however. You can't use a property that returns a UDT in a With block without crashing the Visual Basic environment. I hope this bug will be fixed in a future service pack.

Special OLE data types

Properties can also return a few special data types. For example, the Wizard declares all the color properties using the OLE_COLOR type, as in this code:

Public Property Get BackColor() As OLE_COLOR
    BackColor = Text1.BackColor
End Property

When a property is declared as returning an OLE_COLOR value, programmers can pick its value from a palette of colors in the Properties window, exactly as they can with the ForeColor and BackColor properties of Visual Basic's own controls. For any other purpose, an OLE_COLOR property is treated internally as a Long.

Visual Basic supports three other special data types:

Procedure IDs

A few ActiveX control properties have special meanings. You define such special properties by assigning specific procedure IDs in the Advanced section of the Procedure Attributes dialog box.

As I already explained in the "Attributes" section of Chapter 6, you can make a property or a method the default member of a class by typing 0 (zero) or by selecting the (default) option from the list in the Procedure ID field. An OLE_ OPTEXCLUSIVE property must be the default property to have the ActiveX control correctly behave like an OptionButton control.

If you have a Text or Caption property, you should assign it the Text or Caption procedure ID, respectively. These settings make these properties behave as they do in Visual Basic: When the programmer types their values in the Properties window, the control is immediately updated. Behind the scenes, the Properties window calls the Property Let procedure at each key press instead of calling it only when the programmer presses the Enter key. You can use these procedure IDs for any property, regardless of its name. However, your control can't have more than two properties that behave in this way.

TIP
Because you can select only one item in the procedure ID field, it seems to be impossible to duplicate the behavior of Visual Basic's TextBox and Label controls, which expose a Text or Caption property that's immediately updated by the Properties window and is the default property at the same time. You can work around this problem by defining a hidden property, make it the default property, and have it delegate to the Text or Caption property:

' Make this property the default property, and hide it.
Public Property Get Text_() As String
    Text_ = Text
End Property

Public Property Let Text_(ByVal newValue As String)
    Text = newValue
End Property

You should assign the Enabled procedure ID to the Enabled property of your ActiveX control so that it works correctly. This is a necessary step because the Enabled property behaves differently from any other property. When you disable a form, the form also disables all its controls by setting their Extender's Enabled property to False (so that controls appear disabled to the running code), but without setting their inner Enabled properties to False (so that controls repaint themselves as if they were enabled). To have Visual Basic create an Extender's Enabled property, your UserControl module must expose a Public Enabled property marked with the Enabled procedure ID:

Public Property Get Enabled() As Boolean
    Enabled = Text1.Enabled
End Property

Public Property Let Enabled(ByVal New_Enabled As Boolean)
    Text1.Enabled() = New_Enabled
    PropertyChanged "Enabled"
End Property

The ActiveX Control Interface Wizard correctly creates the delegation code, but you have to assign the Enabled procedure ID manually.

Finally, you can create an About dialog box for displaying copyright information about your control by adding a Public Sub in its UserControl module and assigning the AboutBox procedure ID to it:

Sub ShowAboutBox()
    MsgBox "The SuperTextBox control" & vbCr _
        & "(C) 1999 Francesco Balena", vbInformation
End Sub

When the ActiveX control exposes a method with this procedure ID, an (About) item appear in the Properties window. It's common practice to hide this item so that programmers aren't encouraged to call it from code.

The Procedure Attributes dialog box

A few more fields in the Procedure Attributes dialog box are useful for improving the friendliness of your ActiveX controls. Not one of these setting affects the functionality of the control.

I've already described the Don't Show In Property Browser field in the "Design-Time and Run-Time Properties" section earlier in this chapter. When this check box is selected, the property won't appear in the Properties window at design time or in the Locals window at run time.

The Use This Page In The Property Browser combo box lets you associate the property with one generic property page provided by Visual Basic (namely StandardColor, StandardDataFormat, StandardFont, and StandardPicture) or with a property page that's defined in the ActiveX control project. When a property is associated with a property page, it appears in the Properties window with a button that, when clicked, brings up the property page. Property pages are described later in this chapter.

Use the Property Category field to select the category under which you want the property to appear in the Categorized tab of the Properties window. Visual Basic provides several categories—Appearance, Behavior, Data, DDE, Font, List, Misc, Position, Scale, and Text—and you can create new ones by typing their names in the edit portion of this combo box.

The User Interface Default attribute can have different meanings, depending on whether it's applied to a property or to an event. The property marked with this attribute is the one that's selected in the Properties window when you display it after creating the control. The event marked with the User Interface Default attribute is the one whose template is built for you by Visual Basic in the code window when you double-click the ActiveX control on the form's surface.

Limitations and workarounds

Creating ActiveX controls based on simpler constituent controls is an effective approach, but it has its limits as well. The one that bothers me most is that there's no simple way to create controls that expand on TextBox or ListBox controls and correctly expose all of their original properties. Such controls have a few properties—for example, MultiLine, ScrollBars, and Sorted—which are read-only at run time. But when you place an ActiveX control on a form at design time, the ActiveX control is already running, so you can't modify those particular properties in the Properties window of the application that's using the control.

You can use a few tricks to work around this problem, but none of them offers a definitive solution. For example, sometimes you can simulate the missing property with code, such as when you want to simulate a ListBox's Sorted property. Another well-known trick relies on an array of constituent controls. For example, you can implement the MultiLine property by preparing both a single-line and multiline TextBox controls and make visible only the one that matches the current property setting. The problem with this approach is that the number of needed controls grows exponentially when you need to implement two or more properties in this way. You need 5 TextBox controls to implement the MultiLine and ScrollBars properties (one for single-line TextBox controls and 4 for all the possible settings of the ScrollBar property), and 10 TextBoxes if you also want to implement the HideSelection property.

A third possible solution is to simulate the control that you want to implement with simpler controls. For example, you can manufacture a ListBox-like ActiveX control based on a PictureBox and a companion VScrollBar. You simulate the ListBox with graphic methods of the PictureBox, so you're free to change its graphic style, add a horizontal scroll bar, and so on. Needless to say, this solution isn't often simple.

I want merely to hint of a fourth solution, undoubtedly the most complex of the lot. Instead of using a Visual Basic control, you create a control from thin air using the CreateWindowEx API function. This is the C way, and following this approach in Visual Basic is probably even more complicated than working in C because the Visual Basic language doesn't offer facilities, such as pointers, that are helpful when you're working at such a low level.

After hearing all these complaints, you'll be happy to know Visual Basic 6 has elegantly solved the problem. In fact, the new Windowless control library (described in Chapter 9) doesn't expose a single property that's read-only at run time. The only drawback of this approach is that in that library controls don't expose an hWnd property, so you can't augment their functionality using API calls, which I describe in the Appendix.

Container Controls

You can create ActiveX controls that behave like container controls, as PictureBox and Frame controls do. To manufacture a container control, all you have to do is set the UserControl's ControlContainer property to True. Keep in mind, however, that not all host environments support this feature. If the container doesn't support the ISimpleFrame interface, your ActiveX control won't be able to contain other controls, even if it works normally as far as other features are concerned. Visual Basic's forms support this interface, as do PictureBox and Frame controls. In other words, you can place an ActiveX control that works as a container inside a PictureBox or Frame control, and it will work without a glitch.

You can place controls on a container control both at design time (using drag-and-drop from the ToolBox) or at run time (through the Container property). In both cases, the ActiveX control can find out which controls are placed on its surface by querying its ContainedControls property. This property returns a collection that holds references to the Extender interface of the contained controls.

On the companion CD, you'll find a simple container ActiveX control named Stretcher, which automatically resizes all the contained controls when it's resized. The code that implements this capability is unbelievably simple:

' These properties hold the previous size of the control.
Private oldScaleWidth As Single
Private oldScaleHeight As Single

' To initialize the variables, you need to trap both these events.
Private Sub UserControl_InitProperties()
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub

Private Sub UserControl_Resize()
    ' When the UserControl resizes, move and resize all container controls.
    Dim xFactor As Single, yFactor As Single
    ' Exit if this is the first resize.
    If oldScaleWidth = 0 Then Exit Sub
    ' This accounts for controls that can't be resized.
    On Error Resume Next
    ' Determine the zoom or factor along both axis.
    xFactor = ScaleWidth / oldScaleWidth
    yFactor = ScaleHeight / oldScaleHeight
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
    
    ' Resize all controls accordingly.
    Dim ctrl As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
    Next
End Sub

The ContainedControls collection includes only the contained controls that had been placed directly on the UserControl's surface. For example, if the ActiveX control contains a PictureBox, which in turn contains a TextBox, the PictureBox appears in the ContainedControls collection but the TextBox doesn't. Using Figure 17-8 as a reference, this means that the preceding code stretches or shrinks the Frame1 control contained in the Stretcher ActiveX control, but not the two OptionButton controls inside it. To have the resizing code work as well for the innermost controls, you need to modify the code in the UserControl_Resize event procedure as follows (added statements are in boldface):

Dim ctrl As Object, ctrl2 As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
        For Each ctrl2 In Parent.Controls
            ' Look for controls on the form that are contained in Ctrl.
            If ctrl2.Container Is ctrl Then
                ctrl2.Move ctrl2.Left * xFactor, ctrl2.Top * yFactor,_
                    ctrl2.Width * xFactor, ctrl2.Height * yFactor
            End If
        Next
    Next

Click to view at full size.

Figure 17-8. The Stretcher ActiveX control resizes all its contained controls, both at design time and at run time.

You should know a few other bits of information about container ActiveX controls authored in Visual Basic:

A problem with container controls is that the UserControl module doesn't receive any events when a control is added or removed at design time. If you need to react to these actions—for example, to automatically resize the contained control—you must use a Timer control that periodically queries the ContainedControls.Count collection. While this approach isn't elegant or efficient, you usually need to activate the Timer only at design time, and therefore you experience no impact on the run-time performance.

Transparent Controls

Visual Basic offers you many ways to create irregularly shaped controls. To begin with, if you set the BackStyle property of the UserControl object to 0-Transparent, the background of the control—that is, the portion of the control that isn't occupied by constituent controls—becomes transparent and lets the user see what's behind the control itself. When a control has a transparent background, all the mouse events go directly to the container form or to the control that happens to be under the ActiveX control in the z-order. In addition, Visual Basic ignores the BackColor and Picture properties for such an ActiveX control and all the output from graphic methods is invisible. Not surprisingly, transparent controls are also more demanding in terms of CPU time because, while repainting, Visual Basic has to clip all the areas that don't belong to the controls.

Using Label and Shape controls

If your transparent control includes one or more Label controls that use a TrueType font and whose BackStyle property is also set to 0-Transparent, Visual Basic clips all the pixels around the characters in the Label. Only the caption of the Label is considered to belong to the ActiveX control, and all the other pixels in the Label are transparent. For example, if you click inside a letter O in the caption, a Click event is raised in the parent form or in the control that shows through. I noticed that this feature works decently only with larger font sizes, however.

You can create a large variety of nonrectangular controls using Shape controls as constituent controls. (You can see one example on the companion CD.) If you set the Shape control's BackStyle property to 0-Transparent, all the pixels that fall outside the Shape control are transparent. For example, to create an elliptical radio button, you drop a Shape1 constituent control, set its Shape property to 2-Oval, and set both the UserControl's and Shape control's BackStyle property to 0-Transparent. Then you need only some code that resizes the Shape control when the UserControl resizes and that refreshes the control's appearance when the Value property changes. Following is a partial listing for the UserControl code module.

' Change the color when the control is clicked.
Private Sub UserControl_Click()
    Value = True
    RaiseEvent Click
End Sub

Private Sub UserControl_Resize()
    Shape1.Move 0, 0, ScaleWidth, ScaleHeight
End Sub

Public Sub Refresh()
    ' TrueColor and FalseColor are Public properties. 
    Shape1.BackColor = IIf(m_Value, TrueColor, FalseColor)
    Shape1.FillColor = Shape1.BackColor
End Sub

' Value is also the default property.
Public Property Get Value() As OLE_OPTEXCLUSIVE
    Value = m_Value
End Property
Public Property Let Value(ByVal New_Value As OLE_OPTEXCLUSIVE)
    m_Value = New_Value
    Refresh
    PropertyChanged "Value"
End Property

The problem with using Shape controls to define irregularly shaped controls is that you can't easily use graphic methods to draw over them. The reason is that Visual Basic redraws the Shape control after raising the Paint event, so the Shape control covers the graphic you've produced in the Paint event. An easy way to work around this limitation is to activate a Timer in the Paint event and let the drawing occur in the Timer's Timer procedure, some milliseconds after the standard Paint event. Use this code as a guideline:

Private Sub UserControl_Paint()
    Timer1.Interval = 1        ' One millisecond is enough.
    Timer1.Enabled = True
End Sub

Private Sub Timer1_Timer()
    Timer1.Enabled = False     ' Fire just once.
    ' Draw some lines, just to show that it's possible.
    Dim i As Long
    For i = 0 To ScaleWidth Step 4
        Line (i, 0)-(i, ScaleHeight)
    Next
End Sub

As far as I know, the only other way to solve this problem is by subclassing the UserControl to run some code after the standard processing of the Paint event. (Subclassing techniques are described in the Appendix.)

Using the MaskPicture and MaskColor properties

If the shape of your transparent control is too irregular to be rendered with one Shape control (or even with a group of Shape controls), your next best choice is to assign a bitmap to the MaskPicture property and then to assign the color that should be considered as transparent to the MaskColor property. The bitmap is used as a mask, and for each pixel in the bitmap whose color matches MaskColor, the corresponding pixel on the UserControl becomes transparent. (Constituent controls are never transparent, even if they fall outside the mask region.) You also need to set the Backstyle property to 0-Transparent for this technique to work correctly.

Using this process, you can create ActiveX controls of any shape, including ones that have holes in them. Probably the only serious limitation of this approach is that you can't easily create a mask bitmap that resizes with the control because you can assign the MaskPicture property a bitmap, GIF, or JPEG image, but not a metafile.

Lightweight Controls

Visual Basic 6 permits you to write lightweight ActiveX controls that consume fewer resources at run time and therefore load and unload faster. The UserControl object exposes two new properties that let you fine-tune this capability.

The HasDC and Windowless properties

The HasDC property determines whether the UserControl creates a permanent Windows device context or uses a temporary device context when the control is redrawn and during event procedures. Setting this property to False can improve performance on systems with less memory. For more information about this property, see the "Fine-Tuning the Performance of Forms" section in Chapter 2.

Setting the Windowless property to True creates an ActiveX control that doesn't actually create a window and therefore consumes even fewer resources. A windowless control has a couple of limitations, however. It must be user-drawn or contain only other windowless controls, and it can't work as a container for other controls. You can't place regular constituent controls on a windowless ActiveX control, and you can't set the Windowless property to True if the UserControl already includes nonwindowless constituent controls. Image, Label, Shape, Line, and Timer are the only intrinsic controls that you can place over a windowless UserControl. If you need features that these controls don't provide, have a look at the Windowless control library mentioned in the "Limitations and Workarounds" section earlier in this chapter.

Not all containers support windowless controls. Among the environments that do are Visual Basic 5 and 6, Internet Explorer 4 or later, and all the environments based on Visual Basic for Applications. Interestingly, when a windowless control runs in an environment that doesn't support this feature, the windowless control automatically turns into a regular control that's backed up by a real window.

A windowless control doesn't expose an hWnd property, so you can't call API functions to augment its functionality. (In some cases, you can use the ContainerHwnd property instead.) Moreover, the EditAtDesign and BorderStyle properties are disabled for windowless ActiveX controls. The HasDC property is usually ignored as well because windowless controls never have a permanent device context. But you should set this property to False because if the control runs in an environment that doesn't support windowless ActiveX controls, it won't, at least, use resources for a permanent device context.

Transparent windowless controls

You can create a windowless control that has a transparent background by setting its BackStyle property to 0-Transparent and assigning a suitable bitmap to the MaskPicture. But you should also consider the new HitTest event and the HitBehavior and ClipBehavior properties.

Before I show you how to use these new members, you need to understand what the four regions associated with a control are. (See Figure 17-9.) The Mask region is the nontransparent portion of a control, which includes all the constituent controls and other areas that contain the output from graphic methods. (In regular controls, this is the only existing region.) The Outside region is the area outside the Mask region, while the Transparent region is any area inside the Mask region that doesn't belong to the control (the holes in the control). Finally, the Close region is an area that encircles the Mask region and whose width is determined by the author of the ActiveX control.

Click to view at full size.

Figure 17-9. The four regions associated with a transparent control.

The problem with managing mouse actions over a transparent control is that Visual Basic doesn't know anything about the Close and Transparent regions, and it can only determine whether the mouse cursor is on the Mask region or in the Outside region. The problem is even worse when there are multiple overlapping controls, each one with its own Close or Transparent region, because Visual Basic has to decide which one will receive the mouse event. To let the control decide whether it wants to manage the mouse action, Visual Basic fires one or more HitTest events in all the controls that are under the mouse cursor, in their z-order. (That is, it fires the first event in the control that's on top of all others.) The HitTest event receives the x and y coordinates of the mouse cursor and a HitTest argument:

Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
    ' Here you manage the mouse activity for the ActiveX control.
End Sub

The possible values for HitResult are 0-vbHitResultOutside, 1-vbHitResultTransparent, 2-vbHitResultClose, and 3-vbHitResultHit. Visual Basic raises the HitTest event multiple times, according to the following schema:

Since Visual Basic knows only about the Mask and Outside regions, the value of HitResult that it passes to the HitTest event can only be 0 or 3. If you want to notify Visual Basic that your control has a Close or Transparent region, you must do so by code. In practice, you test the x and y coordinates and assign a suitable value to HitResult, as shown in the following code:

' A control with a circular transparent hole in it.
Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
    Const HOLE_RADIUS = 200, CLOSEREGION_WIDTH = 10
    Const HOLE_X = 500, HOLE_Y = 400
    Dim distance As Single
    distance = Sqr((X _ HOLE_X) ^ 2 + (Y _ HOLE_Y) ^ 2)
    If distance < HOLE_RADIUS Then
        ' The mouse is over the transparent hole.
        If distance > HOLE_RADIUS _ CLOSEREGION_WIDTH Then
            HitResult = vbHitResultClose
        Else
            HitResult = vbHitResultTransparent
        End If
    Else
        ' Otherwise use the value passed to the event (0 or 3).
    End If
End Sub

Not surprisingly, all these operations can add considerable overhead and slow down the application. Moreover, Visual Basic needs to clip the output accounting for the mask defined by MaskPicture for constituent controls and the output of graphic methods. To keep this overhead to a minimum, you can modify Visual Basic's default behavior by means of the ClipBehavior and HitBehavior properties.

The ClipBehavior property affects how Visual Basic clips the output of graphic methods. The default value is 1-UseRegion, which means that the output of a graphic method is clipped to fit the Mask region. The value 0-None doesn't perform clipping at all, and graphic output is visible also on the Mask and Transparent regions.

The HitBehavior property determines how the HitResult argument is evaluated before calling the HitTest event. When HitBehavior = 1-UseRegion (the default value), Visual Basic sets HitResult = 3 only for points inside the Mask region. If you set HitBehavior = 2-UsePaint, Visual Basic also considers the points produced by graphic methods in the Paint event. Finally, if HitBehavior = 0-None, Visual Basic doesn't even attempt to evaluate HitResult and always passes a 0 value to the HitTest event.

If your Mask region isn't complex and you can easily describe it in code, you can often improve the performance of your ActiveX control by setting HitBehavior = 0-UseNone. In this case, Visual Basic always passes 0 to the HitResult argument, and you change it to account for your Mask, Close, and Transparent regions. If the Mask region is complex and includes irregular figures, you should set ClipBehavior = 0-None, thus saving Visual Basic the overhead needed to distinguish between the Mask and Outside regions.

You can easily create controls with hot spots using ClipBehavior = 0-None and HitBehavior = 1-UseRegion. In practice, you draw your control over its entire client area and use the MaskPicture property to define the areas that react to the mouse.

Data Binding

You can add data-binding capabilities to an ActiveX control with little more than a few mouse clicks. As is not the case for intrinsic controls, you can create controls that bind multiple properties to database fields. All you have to do is tick the Property Is Data Bound check box in the Data Binding section of the Procedure Attributes dialog box, shown in Figure 17-10, for all the properties that you want to make data aware.

You can create as many data-bound properties as you like, but you must select the This Property Binds To DataField option for one of them only. If no property is bound to the DataField property, the Extender object won't expose all the Dataxxxx properties that are necessary to actually bind the control. Because such properties are exposed by the Extender object, their availability depends on the host environment.

Figure 17-10. The Procedure Attributes dialog box includes all the options for creating data-aware properties.

PropertyChanged and CanPropertyChange methods

To support data binding in code, you don't have to do anything more than you already do for persistent properties. In each Property Let procedure, you must call the PropertyChanged method, which informs Visual Basic that the property has changed and that the database field should be updated before the record pointer moves to another record. If you omit this call, the database field won't be updated. You can also update the field immediately if you select the Update Immediate option in the Procedure Attributes dialog box.

Visual Basic also provides the CanPropertyChange method, which queries the data source to determine whether it's safe to update the field. You could use the following code in the Property Let procedure of a property called CustomerName. (The statements that have been added to the code by the wizard are in boldface.)

Public Property Let CustomerName(New_CustomerName As String)
    If CanPropertyChange("CustomerName") Then    
        txtCustomerName.Text = New_CustomerName
        PropertyChanged "CustomerName"
    End If
End Sub

You should be aware, however, that you don't strictly need to call the CanPropertyChange method because under Visual Basic 5 and 6 it always returns True, even if the database field can't be updated. You should use this function only for compatibility with future versions of the language that might implement it. For all the properties that call this method before doing the update, you should also select the Property Will Call CanPropertyChange Before Changing option in the Procedure Attributes dialog box. Again, at this time there's no point in doing that, but it doesn't cause any harm either. The choice is yours.

To correctly support data binding, the constituent controls must update the corresponding bound property when their contents change. Typically this is done in the Change or Click event procedure, as in the following code snippet:

Private Sub txtCustomerName_Change()
    PropertyChanged "CustomerName"
End Sub

The DataBindings collection

As I mentioned before, only one property can be bound to the DataField Extender property. Because you can bind multiple properties, you need to provide developers with a method for associating each bound property to the corresponding database field. This association can be done either at design time or during execution.

For each property that you want to make bindable at design time, you must select the Show In DataBindings Collection At Design Time option in the Procedure Attributes dialog box. If this option is selected for one or more properties, the DataBindings item appears in the Properties window. When you click on it, Visual Basic brings up the dialog box shown in Figure 17-11. Note that it's OK that the property bound to the DataField property also appears in the DataBindings collection.

Visual Basic 6 permits you to bind properties in the DataBindings collection to fields in different Data Sources, and you can also select a distinct DataFormat for each one of them. In Visual Basic 5, you could bind properties only to the same Data Source.

Figure 17-11. The DataBindings dialog box lets developers associate properties with database fields at design time.

All the bound properties appear in the DataBindings collection at run time, regardless of whether they appear in the collection at design time. You can't add new items to this collection through code, but you can change the database field to which a property is bound:

' Bind the CustomerName property to the CompanyName database field.
Customer1.DataBindings("CustomerName").DataField = "CompanyName"

Another common task for the DataBindings collection is to cancel changes in fields so that the database record won't be updated:

Dim dtb As DataBinding
For Each dtb In Customer1.DataBindings
    dtb.DataChanged = False
Next

For more information about the DataBindings collection, see the online Visual Basic documentation.

The DataRepeater control

Visual Basic 6 lets you create custom grid-like controls, using the DataRepeater control (contained in the Msdatrep.ocx file). This control works as a container of other ActiveX controls: It can host any type of controls, but it's especially useful with custom ActiveX controls.

Say that you want to display a table of records, but you don't want to use a standard Visual Basic grid control—such as the DataGrid or Hierarchical FlexGrid control—because you need maximum flexibility for interaction with the user or because you want to display information that can't be embedded in a regular grid (images, for example). Figure 17-12 shows a custom grid built on the DataRepeater control that displays the Publisher table from the Biblio.mdb database. To create such a custom grid, you must execute these basic steps:

  1. Create an AddressOCX control that contains all the fields you need; this is the object that will be replicated in the DataRepeater control.
  2. For all the properties that you want to expose in the DataRepeater control—that is, Name, Street, City, Zip, and State—make the property data bound and have it appear in the DataBindings collection at design time.
  3. Save the project, compile it into a stand-alone OCX file, and load the client application where you want to display the custom grid.
  4. Drop an ADO Data control on the client form, and then set its ConnectionString and RecordSource properties to point to the table in the database that provides the data. (You can also use any other ADO data source, including a DataEnvironment object.)
  5. Drop a DataRepeater control on the form, have its DataSource property pointing to the ADO Data control, and select the AddressOCX ActiveX control from the list that appears when you click on the RepeatedControlName. (This list includes all the OCXs that are registered in your system.)
  6. Bring up the DataRepeater control's custom property page, switch to the RepeaterBindings tab, and associate the bound properties exposed by the inner ActiveX control with the database fields. You can also set in the Format tab the DataFormat property for each field.

Click to view at full size.

Figure 17-12. The DataRepeater control lets you create custom views of your database tables.

The complete source code of the demonstration program is on the companion CD.

The DataRepeater control has some rough edges, and you must pay attention to many details to have it working properly:

The DataRepeater control exposes several properties, methods, and events that augment its potential and flexibility. For example, you can directly access the active instance of the child control to set additional properties (RepeatedControl property), find the line number of the current record (ActiveRow property), change the DataRepeater's appearance (by assigning the Caption, CaptionStyle, ScrollBars, RowIndicator, and RowDividerStyle properties), get or set a bookmark to the current or the visible records (using the CurrentRecord and VisibleRecords properties), and so on. You can also monitor users' actions—for example, when they scroll the contents of the list (ActiveRowChanged and VisibleRecordsChanged events) or select another row (CurrentRecordChanged event).

Interestingly, it's even possible to load a different child ActiveX control at run time by assigning a new value to the RepeatedControlName property. In this case, you must associate the bound property with fields by using the properties of the RepeaterBindings collection. (You can provide the user with a list of bindable properties using the PropertyNames property.) Whenever a new child control is loaded at run time, the DataRepeater fires a RepeatedControlLoaded event, which the programmer can use to correctly initialize the new control.

What's missing

The data binding mechanism offered by Visual Basic is fairly complete, although a few features aren't directly supported and you have to implement them yourself.

For example, there's no direct support for controls that bind a list of values to a secondary Data source, as the DataList and DataCombo controls do. You can implement this feature by exposing a custom property—such as RowSource—to which developers can assign the secondary Data control (or another ADO-compliant data source). Here the problem to solve is: You can't display a custom list in the Properties window, so how do you let the developer select the data source at design time? The answer is based on custom property pages, which are described in the next section.

One thing that at first seems to be impossible is to decide at run time which property binds to the DataField Extender property. In this situation, the solution is actually simpler than it might appear: Create an additional property that binds to DataField and that delegates to one of the other properties exposed by the control. This mechanism can be made extremely flexible by means of the new CallByName function. For example, let's say that you want to give developers the ability to bind any property among those exposed by the Customer control. You need to create two additional properties: BoundPropertyName, which holds the name of the bound property, and BoundValue, which does the actual delegation. This is the code in the Property Get and Let procedures for the latter property:

' BoundValue binds directly to DataField, but the value actually stored
' in the database depends on the BoundPropertyName property.
Public Property Get BoundValue() As Variant
    BoundValue = CallByName(Me, BoundPropertyName, vbGet)
End Property

Public Property Let BoundValue (New_BoundValue As Variant)
    CallByName Me, BoundPropertyName, vbLet, New_BoundValue
End Property

You should make BoundValue hidden so that developers are discouraged from using it directly.

Property Pages

The majority of ActiveX controls that you find in the Visual Basic package or buy from third-party vendors are equipped with one or more custom property pages. In this section, you'll see how easy it is to create property pages for your own ActiveX controls.

Even if the Visual Basic's Properties window is usually sufficient to enter property values at design time, there are at least three reasons why you should create custom property pages. First, they greatly simplify the job of the programmers that are using your control because all properties can be grouped in a logical way. Second, and more important, property pages give you much greater influence over how properties are set at design time. For example, you can't show a combo box in the Properties window with a list of values built dynamically, nor can you let developers drop down a mini-editor to enter multiple values (as they do when editing the List property of ListBox and ComboBox controls). These restrictions are easily overcome with property pages. Third, property pages permit you to localize the design-time user interface of your controls for different languages.

So that you can see property pages in action, I created a SuperListBox ActiveX control, an expanded ListBox that exposes an AllItems property (which returns all the items separated by a carriage return character) and allows you to enter new items at run time using a pop-up menu. My control also gives the programmer the ability to bind either the Text property or the ListIndex property to the DataField, thus overcoming one of the few limitations of the data binding mechanism in Visual Basic. This control employs a number of interesting programming techniques—such as API functions to implement a columnar format—and you might want to browse its source code on the companion CD.

Running the Property Page Wizard

You can add a property page to an ActiveX Control project with the Add Property Page command from the Project menu, but you can save a lot of work and time using the Property Page Wizard. (You have to install this add-in from the Add-In Manager dialog box.) In the first step of the wizard, you can create custom property pages, select their order, and decide whether you want to keep standard property pages. (See Figure 17-13.) Visual Basic automatically adds the StandardColor, StandardFont, and StandardPicture pages (for properties that return OLE_COLOR, StdFont, and StdPicture values, respectively), but you can also decide to deactivate them if you want.

Click to view at full size.

Figure 17-13. The first step of the Property Page Wizard is the point at which you create new pages and change the order of selected pages.

In the second step of the wizard, you decide on which page each custom property will be displayed. All the properties that you leave in the leftmost list box (as shown in Figure 17-14) won't be displayed on any property page.

Click to view at full size.

Figure 17-14. In the second step of the Property Page Wizard, you decide which properties will be shown on which page.

When you click on the Finish button, the wizard creates one or more PropertyPage modules. For each property that you assigned to the page, the wizard generates a Label control (whose Caption is the name of the property) and a TextBox control that holds the value of the property, or a CheckBox control if the property returns a Boolean value. If you want a fancier user interface—for example, ComboBox controls for enumerated properties—you have to modify what the wizard has produced. Figure 17-15 shows the General property page for the SuperListBox control after I rearranged the controls and converted a couple of TextBox controls into ComboBox controls.

Click to view at full size.

Figure 17-15. The property page generated by the Property Page Wizard, after some retouching.

The PropertyPage object

Just browsing the code produced by the wizard is sufficient to understand how property pages work. The PropertyPage object is similar to a form and supports many of the Form object's properties, methods, and events, including Caption, Font, and all the keyboard and mouse events. You might even implement property pages that work as drag-and-drop servers or clients if you need to.

Property pages have their peculiarities, of course. For one, you can control the size of the page using the StandardSize property, which can be assigned one of the values 0-Custom (the size is determined by the object), 1-Small (101-by-375 pixels), or 2-Large (179-by-375 pixels). Microsoft suggests that you create custom-sized pages that aren't larger than the space that you actually need because values other than 0-Custom might display incorrectly at different screen resolutions.

You might notice in Figure 17-15 that the property page doesn't include the OK, Cancel, and Apply buttons that you usually find on standard property pages. Those buttons, in fact, are provided by the environment, and you don't have to add them yourself. The communication between the property page and the environment occurs through properties and events of the PropertyPage object. If the project is associated with a help file, a Help button is also displayed.

When the page loads, the PropertyPage object receives the SelectionChanged event. In this event, your code should load all the controls in the page with the current values of the corresponding properties. The SelectedControls collection returns a reference to all the controls in the form that are currently selected and that will be affected by the property page. For example, this is the code in the SelectionChanged event procedure for the General page of the SuperListBox control:

Private Sub PropertyPage_SelectionChanged()
    txtCaption.Text = SelectedControls(0).Caption
    txtAllItems.Text = SelectedControls(0).AllItems
    chkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
    cboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
    cboBoundPropertyName.Text = SelectedControls(0).BoundPropertyName
    Changed = False
End Sub

When the contents of any field on the page is modified, the code in its Change or Click event should set the PropertyPage's Changed property to True, as in these examples:

Private Sub txtCaption_Change()
    Changed = True
End Sub

Private Sub cboShowPopupMenu_Click()
    Changed = True
End Sub

Setting the Change property to True automatically enables the Apply button. When the user clicks on this button (or simply switches to another property page), the PropertyPage object receives an ApplyChanges event. In this event, you must assign the values on the property page to the corresponding ActiveX control's properties, as in the following example:

Private Sub PropertyPage_ApplyChanges()
    SelectedControls(0).Caption = txtCaption.Text
    SelectedControls(0).AllItems = txtAllItems.Text
    SelectedControls(0).Enabled = chkEnabled.Value
    SelectedControls(0).ShowPopupMenu = cboShowPopupMenu.ListIndex
    SelectedControls(0).BoundPropertyName = cboBoundPropertyName.Text
End Sub

One more custom event is associated with PropertyPage objects—the EditProperties event. This event fires when the property page is displayed because the developer clicked on the ellipsis button beside a property name in the Properties window. (This button appears if the property has been associated with a specific property page in the Procedure Attributes dialog box.) You usually take advantage of this property to automatically move the focus on the corresponding control on the property page:

Private Sub PropertyPage_EditProperty(PropertyName As String)
    Select Case PropertyName
        Case "Caption"
            txtCaption.SetFocus
        Case "AllItems"
            txtAllItems.SetFocus
        ' etc. (other properties omitted...)
    End Select
End Sub

You might also want to disable or hide all other controls on the page, but this is rarely necessary or useful.

Working with multiple selections

The code produced by the Property Page Wizard accounts for only the simplest situation—that is, when only one ActiveX control is selected on the form. To build robust and versatile property pages, you should make them work also with multiple controls. Keep in mind that property pages aren't modal, and therefore the developer is allowed to select (or deselect) controls on the form even when the page is already visible. Each time a new control is added to or removed from the SelectedControls collection, a SelectionChanged event fires.

The standard way to deal with multiple selections is as follows. If the selected controls on the form share the same value for a given property, you fill the corresponding field on the property page with that common value; otherwise, you leave the field blank. This is a modified version of the SelectionChanged that accounts for multiple selections:

Private Sub PropertyPage_SelectionChanged()
    Dim i As Integer
    ' Use the property of the first selected control.
    txtCaption.Text = SelectedControls(0).Caption
    ' If there are other controls, and their Caption property differs from
    ' the Caption of the first selected control, clear the field and exit.
    For i = 1 To SelectedControls.Count - 1
        If SelectedControls(i).Caption <> txtCaption.Text Then
            txtCaption.Text = ""
            Exit For
        End If
    Next

    ' The AllItems property is dealt with in the same way (omitted ...).
    
    ' The Enabled property uses a CheckBox control. If values differ, use
    ' the special vbGrayed setting. 
    chkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
    For i = 1 To SelectedControls.Count - 1
        If (SelectedControls(i).Enabled And vbChecked) <> chkEnabled.Value 
            Then
            chkEnabled.Value = vbGrayed
            Exit For
        End If
    Next

    ' The ShowPopupMenu enumerated property uses a ComboBox control.
    ' If values differ, set the ComboBox's ListIndex property to _1.
    cboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
    For i = 1 To SelectedControls.Count - 1
        If SelectedControls(i).ShowPopupMenu <> cboShowPopupMenu.ListIndex 
            Then
            cboShowPopupMenu.ListIndex = -1
            Exit For
        End If
    Next

    ' The BoundPropertyName property is dealt with similarly (omitted ...).

    Changed = False
    txtCaption.DataChanged = False
    txtAllItems.DataChanged = False
End Sub

The DataChange properties of the two TextBox controls are set to False because in the ApplyChange event you must determine whether the developer entered a value in either of those fields:

Private Sub PropertyPage_ApplyChanges()
    Dim ctrl As Object
    ' Apply changes to Caption property only if the field was modified.
    If txtCaption.DataChanged Then
        For Each ctrl In SelectedControls
            ctrl.Caption = txtCaption.Text
        Next
    End If
    ' The AllItems property is deal with in the same way (omitted ...).
    
    ' Apply changes to the Enabled property only if the CheckBox control
    ' isn't grayed out.
    If chkEnabled.Value <> vbGrayed Then
        For Each ctrl In SelectedControls
            ctrl.Enabled = chkEnabled.Value
        Next
    End If

    ' Apply changes to the ShowPopupMenu property only if an item 
    ' in the ComboBox control is selected.
    If cboShowPopupMenu.ListIndex <> -1 Then
        For Each ctrl In SelectedControls
            ctrl.ShowPopupMenu = cboShowPopupMenu.ListIndex
        Next
    End If
    ' The BoundPropertyName property is dealt with similarly (omitted ...).
End Sub

Advanced techniques

I want to mention a few techniques that you can use with property pages and that aren't immediately obvious. For example, you don't need to wait for the ApplyChanges event to modify a property in selected ActiveX controls: You can update a property right in the Change or Click event of the corresponding control on the property page. You can therefore achieve in the property page the same behavior that you can implement in the Properties window by assigning a property the Text or Caption procedure ID.

Another easy-to-overlook feature is that the PropertyPage object can invoke Friend properties and methods of the UserControl module because they're in the same project. This gives you some additional flexibility: For example, the UserControl module can expose one of its constituent controls as a Friend Property Get procedure so that the Property Page can directly manipulate its attributes, as you can see in the code at below.

' In the SuperListBox UserControl module
Friend Property Get Ctrl_List1() As ListBox
    Set Ctrl_List1 = List1
End Property

A minor annoyance of this approach is that the PropertyPage code accesses the UserControl through the SelectedControls collection, which returns a generic Object, whereas Friend members can only be accessed through specific object variables. You can work around this issue by casting the elements of the collection to specific object variables:

' In the PropertyPage module
Dim ctrl As SuperListBox
' Cast the generic control to a specific SuperListBox variable.
Set ctrl = SelectedControls(0)
' Now it is possible to access Friend members.
ctrl.Ctrl_List1.AddItem "New Item"

The last technique that I'm showing you is likely to be useful when you're developing complex UserControls with many properties and constituent controls, such as the Customer ActiveX control that I introduced earlier in this chapter. Surprisingly, it turns out that you can use the UserControl even on a property page that's associated with itself. Figure 17-16 shows an example of this technique: The General property page uses an instance of the Customer ActiveX control to let the developer assign the properties of the Customer control itself!

Click to view at full size.

Figure 17-16. A property page that uses an instance of the UserControl object defined in its own project.

The beauty of this approach is how little code you need to write in the PropertyPage module. This is the complete source code of the property page shown in Figure 17-16:

Private Sub Customer1_Change(PropertyName As String)
    Changed = True
End Sub

Private Sub PropertyPage_ApplyChanges()
    ' Read all properties in one loop.
    Dim propname As Variant
    For Each propname In Array("CustomerName", "Address", "City", _
        "ZipCode", "Country", "Phone", "Fax")
        CallByName SelectedControls(0), propname, VbLet, _
            CallByName(Customer1, propname, VbGet)
    Next
End Sub

Private Sub PropertyPage_SelectionChanged()
    ' Assign all properties in one loop.
    Dim propname As Variant
    For Each propname In Array("CustomerName", "Address", "City", _
        "ZipCode", "Country", "Phone", "Fax")
        CallByName Customer1, propname, VbLet, _
            CallByName(SelectedControls(0), propname, VbGet)
    Next
End Sub

Notice how the code takes advantage of the CallByName function to streamline multiple assignments to and from the properties in the UserControl.

The Tricks of the Masters

At this point, you know everything you need to create ActiveX controls that match or even exceed the quality of commercial controls. There are a few advanced techniques, however, that even many experienced programmers aren't aware of. As I'll prove in this section, you don't always need to know all the intricacies of Windows and ActiveX programming to deliver efficient controls because, in most cases, Visual Basic is all you need.

Callback methods

Raising an event in the parent form from within an ActiveX control is easy, but it isn't the only method you can use to let the two objects communicate with each other. In Chapter 16, I showed you how an object can notify another object that something has occurred by using callback methods. Callback methods have several advantages over events: They're about 5 or 6 times faster on average and, more important, they aren't blocked when the client form is showing a message box in an interpreted program.

On the companion CD, you'll find the complete source code for the SuperTimer ActiveX control, which implements a Timer that can communicate with its parent form using a callback mechanism based on the ISuperTimerCBK interface (a PublicNotCreatable class contained in the ActiveX control project). When a form or any other container implements this interface, it can have the SuperTimer control send its notifications through that interface's only member, the Timer method. This is the source code for a typical form that uses this SuperTimer control:

Implements ISuperTimerCBK

Private Sub Form_Load()
    Set SuperTimer1.Owner = Me
End Sub

Private Sub ISuperTimerCBK_Timer()
    ' Do whatever you want here.
End Sub

The SuperTimer control contains a Timer1 constituent control that raises a Timer event in the UserControl module; in this procedure, the control decides whether it has to raise an event or invoke a callback method:

Public Owner As ISuperTimerCBK

Private Sub Timer1_Timer()
    If Owner Is Nothing Then
        RaiseEvent Timer      ' Fire a regular event.
    Else
        Owner.Timer           ' Fire a callback method.
    End If
End Sub

Interestingly, in an interpreted program the Timer event in a standard Timer control doesn't fire if the client form is showing a message box. (Timers are never blocked in compiled programs, though.) You don't suffer from this limitation if you use the ISuperTimerCBK interface of the SuperTimer OCX control, which therefore proves to be more powerful than a regular Timer control. (See Figure 17-17.) But you have to compile the SuperTimer control into an OCX file for this feature to work properly. (When the UserControl module runs in the Visual Basic IDE, modal windows in the client applications block events also in the ActiveX control.)

TIP
The demonstration program of the SuperTimer control displays different messages if the application is running in the IDE or as a compiled program. The Visual Basic language lacks a function that lets you distinguish between the two modes, but you can take advantage of the fact that all the methods of the Debug object aren't compiled in EXE programs and therefore are executed only when the application is running in the IDE. Here's an example of this technique:

Function InterpretedMode() As Boolean
    On Error Resume Next
    Debug Print 1/0                 ' This causes an error
    InterpretedMode = (Err <> 0)    ' but only inside the IDE.
    Err Clear                       ' Clear the error code. 
End Function

The preceding code is based on a routine that appeared in the Tech Tips supplement of the Visual Basic Programmer's Journal.

Figure 17-17. A compiled SuperTimer control can send callback methods to the parent form even if a message box is being displayed.

Faster calls with VTable binding

As you know, all references to external ActiveX controls—but not intrinsic Visual Basic controls—implicitly use their Extender objects. What you probably don't know is that all references to the Extender use early ID binding instead of the most efficient VTable binding. This means that calling a method in an ActiveX control is slower than calling the same method if the object were encapsulated in an ActiveX DLL component because objects in DLLs are referenced through VTable binding.

In general, ID binding doesn't seriously impair the performance of your ActiveX control because most properties and methods implement the user interface and are sufficiently fast even on low-end machines. But sometimes you might need more speed. Say that you have a ListBox control that you want to fill as rapidly as possible with data read from a database or an array in memory: in this situation, you need to call a property or a method several thousand times, and the overhead of ID binding wouldn't be negligible.

A solution to this problem is conceptually simple. You add a PublicNotCreatable class to your ActiveX Control project that exposes the same properties and methods as those exposed by the ActiveX control. The class does nothing but delegate the execution of the properties and methods to the main UserControl module. Whenever the ActiveX control is instantiated, it creates a companion Public object and exposes it as a read-only property. The client form can store the return value of this property in a specific object variable and call the ActiveX control's members through this secondary object. This object doesn't use the Extender object and therefore can be accessed through VTable binding instead of ID binding.

I found that accessing UserControl's properties through this companion object can be about 15 times faster than through the regular reference to the ActiveX control. On the companion CD, you'll find a demonstration project whose only purpose is to show you what kind of performance you can get using this approach. You can use it as a model to implement this technique in your own ActiveX control projects.

Secondary interfaces

An alternative way to use VTable binding for super-fast ActiveX controls is to have the ActiveX control implement a secondary interface and have the client form access the secondary interface instead of the primary interface. This approach is even faster than the one based on a secondary PublicNotCreatable object because you don't need a separate class that delegates to the main ActiveX control module. Another benefit of this approach is that the same interface can be shared by multiple ActiveX controls so that you can implement a VTable-based polymorphism among different but related ActiveX controls.

The implementation of this approach isn't difficult, but beware of one difficulty. Say that you create an ActiveX control that contains an Implements IControlInterface statement at the beginning of its code module. Your goal is to take advantage of this common interface in the client form by assigning a specific ActiveX control instance to an interface variable. Unfortunately, the following sequence of statements raises an error:

' In the client form
Dim ctrl As IControlInterface
Set ctrl = MyControl1                      ' Error "Type Mismatch"

The problem, of course, is that the MyControl1 object in the client code uses the ActiveX control's Extender interface, which doesn't inherit the IControlInterface interface. To access that interface, you need to bypass the Extender object, as follows:

Set ctrl = MyControl1.Object 

Trapping events with multicasting

Multicasting lets you trap events raised by any object that you can reference through an object variable. (I described multicasting in Chapter 7, so you might want to review those pages before reading what follows.) The good news is that multicasting also works with ActiveX controls, even if a control has been compiled into a stand-alone OCX file. In other words, your ActiveX control can trap events fired by the parent form, or even by other controls on the form itself.

To give you a taste of what you can do with this technique, I have prepared a simple ActiveX control that automatically resizes itself to cover the entire surface of its parent form. If it weren't for multicasting, this feature would be extremely difficult to implement because it requires you to subclass the parent form to be notified when it's being resized. Thanks to multicasting, the amount of code you need to implement this feature is amazingly little:

Dim WithEvents ParentForm As Form

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    On Error Resume Next         ' In case parent isn't a form.
    Set ParentForm = Parent
End Sub

' This event fires when the parent form resizes.
Private Sub ParentForm_Resize()
    Extender.Move 0, 0, Parent.ScaleWidth, Parent.ScaleHeight
End Sub

The multicasting technique has an infinite number of applications. For example, you can build an ActiveX control that always displays the sum of the values contained in TextBox controls on the form. For this task, you need to trap those controls' Change events. When trapping the events of an intrinsic control, your UserControl module must declare a WithEvents variable of a specific object type, but when trapping events from external ActiveX controls—for example, a TreeView or MonthView control—you can use a generic VBControlExtender object variable and rely on its one-size-fits-all ObjectEvent event.